﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Collections;
using System.ComponentModel.DataAnnotations;
using System.ServiceModel.DomainServices.Server;
using ExtensionMethods;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Configuration;
using RiaToDalLinq.Contracts;
using RiaToDalLinq.Configuration;
using RiaToDalLinq.ProviderBase;
using RiaToDalLinq.Attributes;
using RiaToDalLinq.DalResolvers;
using RiaToDalLinq.DomainServices;
using System.Runtime.InteropServices;
using RiaToDalLinq.Misc;

namespace RiaToDalLinq
{
    /// <summary>
    /// 
    /// </summary>
    internal class RiaToDalQueryProvider : BaseQueryProvider
    {
        /// <summary>
        /// Key/Value pair that maps a RIA entity to DAL entity
        /// </summary>
        private KeyValuePair<Type, Type> _entityMap;

        /// <summary>
        /// Dictionary that maps RIA properties to DAL properties
        /// </summary>
        private Dictionary<string, string> _propertyMappings;

        /// <summary>
        /// The RIA services query description which provides the method signature used
        /// to resolve the DAL manager method to invoke
        /// </summary>
        private QueryDescription _queryDescription;

        /// <summary>
        /// The domain service that issued the query
        /// </summary>
        private LegacyDalDomainServiceBase _callingDomainService;

        /// <summary>
        /// Query translator to translate the RIA LINQ query and extract SQL whereClause/orderBy/pageSize/pageIndex
        /// parameters, which will be passed to the legacy manager
        /// </summary>
        private RiaToDalQueryTranslator _queryTranslator;

        /// <summary>
        /// 
        /// </summary>
        private List<string> _enumerableListNames = new List<string>();

        private IDalResolver _resolver = null;


        /// <summary>
        /// 
        /// </summary>
        /// <param name="entityMap"></param>
        /// <param name="propertyMappings"></param>
        /// <param name="queryDescription"></param>
        /// <param name="callingDomainService"></param>
        internal RiaToDalQueryProvider(KeyValuePair<Type, Type> entityMap, Dictionary<string, string> propertyMappings, QueryDescription queryDescription, LegacyDalDomainServiceBase callingDomainService)
        {
            // Set the private members
            _entityMap = entityMap;
            _propertyMappings = propertyMappings;
            _queryDescription = queryDescription;
            _callingDomainService = callingDomainService;
            _enumerableListNames.Add("List");
            _resolver = DalResolverProvider.Current;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public override string GetQueryText(System.Linq.Expressions.Expression expression)
        {
            return this.Translate(expression);
        }


        /// <summary>
        /// Executes a RIA to Legacy query
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public override object Execute(System.Linq.Expressions.Expression expression)
        {
            expression = Evaluator.PartialEval(expression);
            this.Translate(expression);
            ArrayList returnValue = new ArrayList();
            _callingDomainService.TotalRecordCount = null;

            //get the calling services method information and it's method mapping information
            MethodInfo callingDomainServiceQueryMethodInfo = _callingDomainService.GetType().GetMethod(_queryDescription.Method.Name);
            MethodMap methodAttribute = Util.GetAttribute<MethodMap>(callingDomainServiceQueryMethodInfo);
          
            if (methodAttribute == null)
            {
                throw new Exception("");
            }

            object propertyReference = _resolver.GetManagerInstance(_entityMap.Value.Name);

            //gets method info for all dal methods on the above manager where the name of the method matches
            //the name defined in the method map attribute of the domain service
            IEnumerable<MethodInfo> methodInformation = propertyReference.GetType().GetMethods().Where(x => x.Name == methodAttribute.MethodName.Replace("(", string.Empty).Replace(")", string.Empty));


            bool matchFound = false;

            //look for the candidate method in the methods returned based on parameter type and order
            foreach (MethodInfo methodInfo in methodInformation)
            {
                ParameterInfo[] pi = methodInfo.GetParameters();

                int? recordCountParameterIndex = null;

                //construct a parameter array to pass to the dal method
                object[] parameters = ConstructParameters(_queryDescription.Method, pi, _queryDescription.ParameterValues, out recordCountParameterIndex);
                
                //only try to invoke if the parameter construction was successful (i.e. there was a match with the
                //DAL method.
                if (parameters != null)
                {
                    matchFound = true;

                    //invoke the manager
                    object results = methodInfo.Invoke(propertyReference, parameters);

                    // Set the record count if we get it
                    if (recordCountParameterIndex.HasValue)
                    {
                        _callingDomainService.TotalRecordCount = (int)parameters[recordCountParameterIndex.Value];
                    }

                    //Is this manager returning a VList or a TList?
                    if (methodInfo.ReturnType.FullName.Contains(_enumerableListNames))
                    {
                        //get the count of entities returned
                        int listCount = (int)results.GetType().GetProperty("Count").GetValue(results, null);

                        //for each DAL entity in the result list copy the properties to the destination RIA object
                        for (int index = 0; index < listCount; index++)
                        {
                            object sourceItem = results.GetType().GetMethod("get_Item").Invoke(results, new object[] { index });
                            object destItem = Activator.CreateInstance(_entityMap.Key);
                            CopyProperties(sourceItem, destItem);
                            returnValue.Add(destItem);
                        }

                    }
                    else
                    {
                        //this method returns a single entity. Add the entity to the return value.
                        object sourceItem = results;
                        object destItem = Activator.CreateInstance(_entityMap.Key);
                        CopyProperties(sourceItem, destItem);
                        returnValue.Add(destItem);
                    }

                    //we found the match, do not try to search for anymore methods
                    break;
                }
            }

            if (!matchFound)
            {
                throw new ApplicationException(string.Format("Could not find DAL method match for domain operation '{0}::{1}()'.", _queryDescription.Method.DomainServiceType.Name, _queryDescription.Method.Name));
            }

            //return the entit(y|ies)
            return returnValue;
        }


        /// <summary>
        /// Translates a RIA LINQ query to a DAL compatible query
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        internal string Translate(System.Linq.Expressions.Expression expression)
        {
            _queryTranslator = new RiaToDalQueryTranslator(_entityMap, _propertyMappings);
            return _queryTranslator.Translate(expression);
        }


        /// <summary>
        /// Copies the properties from a DAL entity to the corresponding RIA entity
        /// </summary>
        /// <param name="source"></param>
        /// <param name="dest"></param>
        private void CopyProperties(object source, object dest)
        {
            //get all properties on the RIA object
            PropertyInfo[] properties = dest.GetType().GetProperties();

            foreach (PropertyInfo pi in properties)
            {
                //get the DAL domain entity field this property maps to
                FieldMap fieldMap = Attribute.GetCustomAttribute(pi, typeof(FieldMap), false) as FieldMap;

                if (fieldMap == null)
                {
                }

                //set the value on the RIA entity
                dest.GetType().GetProperty(pi.Name).SetValue(dest, source.GetType().GetProperty(fieldMap.FieldName).GetValue(source, null), null);
            }
        }


        /// <summary>
        /// Constructs the parameters for a legacy BLL call based off of domainOperationParameters and parameterInfo.
        /// If the parameters are not a match, NULL is returned, if they are they are mapped and an object[] array is returned
        /// representing all the parameter values, in the order they need to be passed to the legacy BLL method.
        /// Rules:
        /// Order and type must match. 
        /// The following parameters are mapped automatically when they exist in the method signature
        /// (i.e. they do NOT have to be in the RIA signature:
        /// string whereClause, string orderBy, int? pageIndex, int? pageSize, int rowCount
        /// </summary>
        /// <param name="domainOperationParameters"></param>
        /// <param name="dalParameterInfo"></param>
        /// <param name="parameterValues"></param>
        /// <param name="pageIndex"></param>
        /// <param name="pageSize"></param>
        /// <param name="orderBy"></param>
        /// <param name="whereClause"></param>
        /// <returns></returns>
        private object[] ConstructParameters(DomainOperationEntry method, ParameterInfo[] dalParameterInfo, object[] parameterValues, out int? totalRecordCountParameterIndex)
        {
            ReadOnlyCollection<DomainOperationParameter> domainOperationParameters = _queryDescription.Method.Parameters;

            totalRecordCountParameterIndex = null;

            //dal parameters will always be >= domainOperation parameters
            if (domainOperationParameters.Count > dalParameterInfo.Count())
            {
                return null;
            }

            object[] parameters = new object[dalParameterInfo.Count()];

            
            int i = 0, j = 0;
            MethodInfo methodInfo = _callingDomainService.GetType().GetMethod(_queryDescription.Method.Name);
            OrderByParameter orderByAttribute = Util.GetAttribute<OrderByParameter>(methodInfo);
            WhereClauseParameter whereClauseAttribute = Util.GetAttribute<WhereClauseParameter>(methodInfo);
            PageSizeParameter pageSizeAttribute = Util.GetAttribute<PageSizeParameter>(methodInfo);
            PageStartParameter pageStartAttribute = Util.GetAttribute<PageStartParameter>(methodInfo);
            TotalRecordCountParameter totalRecordCountAttribute = Util.GetAttribute<TotalRecordCountParameter>(methodInfo);
            IQueryable<StaticParameter> ignoreParameters = Util.GetAttributes<StaticParameter>(methodInfo);

            bool skipDefaultOrderBy = orderByAttribute != null;
            bool skipDefaultWhereClause = whereClauseAttribute != null;
            bool skipDefaultPageSize = pageSizeAttribute != null;
            bool skipDefaultPageStart = pageStartAttribute != null;
            bool skipDefaultRecordCount = totalRecordCountAttribute != null;


            foreach (ParameterInfo pi in dalParameterInfo)
            {
                //if the parameter is one of the following we already know how to map without the
                //caller needing to tell us
                StaticParameter staticParameter = ignoreParameters.Where(p => pi.Name.Equals(p.Name, StringComparison.CurrentCultureIgnoreCase)).FirstOrDefault();
                if (ignoreParameters != null && staticParameter != null)
                {
                    // Ignore Parameter
                    parameters[i] = staticParameter.Value;
                    i++;
                    continue;
                }

                if (orderByAttribute != null)
                {
                    if (pi.Name.Equals(orderByAttribute.Name, StringComparison.CurrentCultureIgnoreCase))
                    {
                        parameters[i] = !string.IsNullOrEmpty(_queryTranslator.OrderBy) ? _queryTranslator.OrderBy : orderByAttribute.DefaultValue;
                        i++;
                        continue;
                    }
                }

                if (whereClauseAttribute != null)
                {
                    if (pi.Name.Equals(whereClauseAttribute.Name, StringComparison.CurrentCultureIgnoreCase))
                    {
                        parameters[i] = !string.IsNullOrEmpty(_queryTranslator.WhereClause) ? _queryTranslator.WhereClause : whereClauseAttribute.DefaultValue;
                        i++;
                        continue;
                    }
                }

                if (pageSizeAttribute != null)
                {
                    if (pi.Name.Equals(pageSizeAttribute.Name, StringComparison.CurrentCultureIgnoreCase))
                    {
                        parameters[i] = _queryTranslator.Take.GetValueOrDefault(pageSizeAttribute.DefaultValue.GetValueOrDefault(_resolver.PageSizeParameter.Item2));
                        i++;
                        continue;
                    }
                }

                if (pageStartAttribute != null)
                {
                    if (pi.Name.Equals(pageStartAttribute.Name, StringComparison.CurrentCultureIgnoreCase))
                    {
                        int? start = pageStartAttribute.DefaultValue.GetValueOrDefault(_resolver.PageStartParameter.Item2);

                        if (pageStartAttribute.ActAs == Enumeration.PageStartActAs.PageIndex
                            || (pageStartAttribute.ActAs == Enumeration.PageStartActAs.NotDefined && _resolver.PageStartActingAs == Enumeration.PageStartActAs.PageIndex))
                        {
                            if (_queryTranslator.Skip.HasValue && _queryTranslator.Take.HasValue)
                            {
                                start = _queryTranslator.Skip / _queryTranslator.Take;
                            }
                        }
                        else if (pageStartAttribute.ActAs == Enumeration.PageStartActAs.Skip
                            || (pageStartAttribute.ActAs == Enumeration.PageStartActAs.NotDefined && _resolver.PageStartActingAs == Enumeration.PageStartActAs.Skip))
                        {
                            if (_queryTranslator.Skip.HasValue)
                            {
                                start = _queryTranslator.Skip;
                            }
                        }

                        parameters[i] = start;
                        i++;
                        continue;
                    }
                }

                if (totalRecordCountAttribute != null)
                {
                    if (pi.Name.Equals(totalRecordCountAttribute.Name, StringComparison.CurrentCultureIgnoreCase))
                    {
                        if (!pi.IsOut && !pi.ParameterType.IsByRef)
                        {
                            throw new ApplicationException(string.Format("Parameter '{0}' on domain operation '{1}::{2}()' must be an OUT/REF parameter.", pi.Name, method.DomainServiceType.Name, method.Name));
                        }

                        int? rowCount = new int();
                        parameters[i] = rowCount;
                        totalRecordCountParameterIndex = i;
                        i++;
                        continue;
                    }
                }


                // ////////////////////
                // Default
                // ////////////////////
                if (!skipDefaultWhereClause && pi.Name.Equals(_resolver.WhereClauseParameter.Item1, StringComparison.CurrentCultureIgnoreCase))
                {
                    parameters[i] = _queryTranslator.WhereClause;
                    i++;
                    continue;
                }
                else if (!skipDefaultOrderBy && pi.Name.Equals(_resolver.OrderByParameter.Item1, StringComparison.CurrentCultureIgnoreCase))
                {
                    parameters[i] = !string.IsNullOrEmpty(_queryTranslator.OrderBy) ? _queryTranslator.OrderBy : _resolver.OrderByParameter.Item2;
                    i++;
                    continue;
                }
                else if (!skipDefaultPageStart && pi.Name.Equals(_resolver.PageStartParameter.Item1, StringComparison.CurrentCultureIgnoreCase))
                {
                    int? start = _resolver.PageStartParameter.Item2;

                    if (_resolver.PageStartActingAs == Enumeration.PageStartActAs.Skip)
                    {
                        if (_queryTranslator.Skip.HasValue)
                        {
                            start = _queryTranslator.Skip;
                        }
                    }
                    else    // Default to PageIndex
                    {
                        if (_queryTranslator.Skip.HasValue && _queryTranslator.Take.HasValue)
                        {
                            start = _queryTranslator.Skip / _queryTranslator.Take;
                        }
                    }

                    parameters[i] = start;
                    i++;
                    continue;
                }
                else if (!skipDefaultPageSize && pi.Name.Equals(_resolver.PageSizeParameter.Item1, StringComparison.CurrentCultureIgnoreCase))
                {
                    parameters[i] = _queryTranslator.Take.GetValueOrDefault(_resolver.PageSizeParameter.Item2);
                    i++;
                    continue;
                }
                else if (!skipDefaultRecordCount && pi.Name.Equals(_resolver.RowCountParameter.Item1, StringComparison.CurrentCultureIgnoreCase))
                {
                    if (!pi.IsOut && !pi.ParameterType.IsByRef)
                    {
                        throw new ApplicationException(string.Format("Parameter '{0}' in legacy DAL method called from domain operation '{1}::{2}()' must be an OUT/REF parameter.", pi.Name, method.DomainServiceType.Name, method.Name));
                    }

                    int? rowCount = new int();
                    parameters[i] = rowCount;
                    totalRecordCountParameterIndex = i;
                    i++;
                    continue;
                }


                //find parameter map if it exists. the parameter map will tell us what
                //id to lookup from the code that is passed in
                CodeToIdMap parameterAttribute = null;

                if (domainOperationParameters.Count <= j)
                {
                    return null;
                }

                foreach (object attribute in domainOperationParameters.ElementAt(j).Attributes)
                {
                    if (attribute is CodeToIdMap)
                    {
                        parameterAttribute = attribute as CodeToIdMap;
                        break;
                    }
                }


                //a mapping attribute is defined, therefore we need to lookup the ID value from the code passed in and
                //pass that into the dal method
                if (parameterAttribute != null)
                {
                    if (typeof(string) != domainOperationParameters.ElementAt(j).ParameterType)
                    {
                        throw new ApplicationException(string.Format("CodeToIdMap attributes are only valid on string parameter types. Parameter '{0}' on domain operation '{1}::{2}()' is of type [{3}] which is invalid.", domainOperationParameters.ElementAt(j).Name, method.DomainServiceType.Name, method.Name, domainOperationParameters.ElementAt(j).ParameterType.Name));
                    }
                    parameters[i] = GetIdFromCodeAttribute(parameterAttribute.DestinationType, parameterAttribute.LookupIdField, parameterAttribute.LookupCodeField, (string)parameterValues[j]);
                }
                else
                {
                    //types are not equal, therefore the method signatures are NOT a match. return NULL
                    if (pi.ParameterType != domainOperationParameters.ElementAt(j).ParameterType)
                    {
                        return null;
                    }

                    //copy the parameter
                    parameters[i] = parameterValues[j];
                }

                //move to next element in the RIA and dal method signatures
                j++;
                i++;
            }

            if (j != parameterValues.Count())
            {
                return null;
            }

            //success! we found a match for the method, yay!
            return parameters;
        }

        /// <summary>
        /// Looks up the primary key ID of a lookup field from the code passed in. 
        /// </summary>
        /// <returns></returns>
        private int? GetIdFromCodeAttribute(Type lookupEntityType, string idField, string codeField, string codeValue)
        {
            return _resolver.GetIdFromCode(lookupEntityType, idField, codeField, codeValue);
        }

        /// <summary>
        /// Checks to see if the Property on this entity is marked with the [AssociationAttribute] and is IEnumerable.
        /// If it is and the child entity is a dal entity then
        /// </summary>
        /// <param name="propertyRef">Reference to the property to examine</param>
        /// <param name="propertyInfo">PropertyInfo of the property to examine</param>
        /// <param name="parentDalType">The dal type of the parent object this property is on</param>
        /// <param name="childRiaType"></param>
        /// <param name="childDalType"></param>
        /// <param name="getByMethodInfo"></param>
        /// <returns></returns>
        private bool IsPropertyAssociated(object propertyRef, PropertyInfo propertyInfo, Type parentDalType, out Type childRiaType, out Type childDalType, out MethodInfo getByMethodInfo)
        {
            //check to see if the object is decorated with the [Associate] attribute
            AssociationAttribute associationAttribute = Attribute.GetCustomAttribute(propertyInfo, typeof(AssociationAttribute)) as AssociationAttribute;

            //default to NULL
            childRiaType = null;
            childDalType = null;
            getByMethodInfo = null;

            if (associationAttribute == null)
            {
                return false;
            }

            Type[] genericArgs = propertyRef.GetType().GetGenericArguments();

            if(genericArgs.Count() != 1)
            {
                return false;
            }
            
            //get the dal entity that belongs to the parent entity
            EntityMap entityMap = Attribute.GetCustomAttribute(parentDalType, typeof(EntityMap)) as EntityMap;

            if (entityMap == null)
            {
                return false;
            }

            object propertyReference = _resolver.GetManagerInstance(genericArgs.ElementAt(0).Name);

            //gets method info for all methods on the above manager where the name of the method matches
            //the name defined in the method map attribute of the domain service
            IEnumerable<MethodInfo> methodInformation = propertyReference.GetType().GetMethods().Where(x => x.Name == string.Format("GetBy{0}Id", entityMap.EntityType.Name));

            if (methodInformation == null || methodInformation.Count() != 1)
            {
                return false;
            }

            //validate parameters
            ParameterInfo[] parameters = methodInformation.ElementAt(0).GetParameters();

            if (parameters == null || parameters.Count() != 1 || parameters.ElementAt(0).Name != string.Format("{0}Id", parentDalType.Name))
            {
                return false;
            }

            //set output params
            childRiaType = genericArgs.ElementAt(0);
            childDalType = entityMap.EntityType;
            getByMethodInfo = methodInformation.ElementAt(0);

            //success!
            return true;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="t">The </param>
        /// <param name="id"></param>
        /// <returns></returns>
        private object LoadEntitiesById(Type parentDalType, Type childDalType, int parentId)
        {
            //list of items to return
            ArrayList returnValue = new ArrayList();

            //get the calling services method information and it's method mapping information
            MethodInfo callingDomainServiceQueryMethodInfo = _callingDomainService.GetType().GetMethod(_queryDescription.Method.Name);
            MethodMap methodAttribute = Attribute.GetCustomAttribute(callingDomainServiceQueryMethodInfo, typeof(MethodMap), false) as MethodMap;

            if (methodAttribute == null)
            {
                throw new Exception("");
            }
            
            object propertyReference = _resolver.GetManagerInstance(_entityMap.Value.Name);

            //gets method info for all methods on the above manager where the name of the method matches
            //the name defined in the method map attribute of the domain service
            IEnumerable<MethodInfo> methodInformation = propertyReference.GetType().GetMethods().Where(x => x.Name == methodAttribute.MethodName.Replace("(", string.Empty).Replace(")", string.Empty));

            //look for the candidate method in the methods returned based on parameter type and order
            foreach (MethodInfo methodInfo in methodInformation)
            {
                ParameterInfo[] pi = methodInfo.GetParameters();

                int? recordCountParameterIndex = null;

                //construct a parameter array to pass to the method
                object[] parameters = ConstructParameters(_queryDescription.Method, pi, _queryDescription.ParameterValues, out recordCountParameterIndex);

                //only try to invoke if the parameter construction was successful (i.e. there was a match with the
                //dal method.
                if (parameters != null)
                {
                    //invoke the manager
                    object results = methodInfo.Invoke(propertyReference, parameters);

                    // Set the record count if we get it
                    if (recordCountParameterIndex.HasValue)
                    {
                        _callingDomainService.TotalRecordCount = (int)parameters[recordCountParameterIndex.Value];
                    }

                    //Is this manager returning a VList or a TList?
                    if (methodInfo.ReturnType.FullName.Contains("VList") || methodInfo.ReturnType.FullName.Contains("TList"))
                    {
                        //get the count of entities returned
                        int listCount = (int)results.GetType().GetProperty("Count").GetValue(results, null);

                        //for each dal entity in the result list copy the properties to the destination RIA object
                        for (int index = 0; index < listCount; index++)
                        {
                            object sourceItem = results.GetType().GetMethod("get_Item").Invoke(results, new object[] { index });
                            object destItem = Activator.CreateInstance(_entityMap.Key);
                            CopyProperties(sourceItem, destItem);
                            returnValue.Add(destItem);
                        }

                    }
                    else
                    {
                        //this method returns a single entity. Add the entity to the return value.
                        object sourceItem = results;
                        object destItem = Activator.CreateInstance(_entityMap.Key);
                        CopyProperties(sourceItem, destItem);
                        returnValue.Add(destItem);
                    }

                    //we found the match, do not try to search for anymore methods
                    break;
                }
            }

            return returnValue;
        }
    }
}